package com.wdl.datarest.implementation;

import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.InterruptedException;
import java.net.DatagramPacket;
import java.net.InetAddress;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Thread worker for processing device software request.
 */
@Service
@Scope("prototype")
public class DevSUThread implements Runnable{
    private static Logger log = LoggerFactory.getLogger("DevSUThread");

    /* 
     * The device software binary is around 42K, which requires about 80 packets.
     * There is 150m delay between two packets. It costs around 12s for each software update.
     * This queue need not be too long since device can wait at most up to 12*5 = 1min. 
     */ 
    private static final int DEV_SU_QUEUE_SIZE = 5;

    private volatile boolean stopped;
    private Thread thread;
    private int threadId;

    private final BlockingQueue<DatagramPacket> queue = new ArrayBlockingQueue<DatagramPacket>(DEV_SU_QUEUE_SIZE);
    
    public void setThreadId(int threadId){
        this.threadId = threadId;
    }

    public int getThreadId(){
        return this.threadId;
    }

    public boolean isQueueFull() {
        if (queue.size() >= DEV_SU_QUEUE_SIZE){
            return true;
        }
        return false;
    }

    public void start() {
        log.info("DevSUThread-" + this.threadId + " start()");
        stopped = false;
        thread = new Thread(this, "DevsUThread-" + this.threadId);
        thread.start();
    }

    public void stop() {
        log.info("DevSUThead-" + threadId + " stop()");
        stopped = true;
        thread.interrupt();
    }

    public boolean add(DatagramPacket devData) throws UnsupportedEncodingException {
        // queue.put is blocking, so use queue.offer instead
        boolean insertionOK = queue.offer(devData);

        if (!insertionOK) {
            log.error("DevSUThread-" + this.threadId + " add(), insertion failure, The queue size is now " + queue.size());
            return insertionOK;
        }

        if (log.isDebugEnabled()) {
            log.debug("DevSUThread-" + this.threadId + " add(),  insertion successfully. The queue size is now " + queue.size());
        }

        return insertionOK;
    }

    public void run() {
        while (true) {
            // This call will block until there is at least one event
            try {
                DatagramPacket devDataItem = queue.take();
                if (devDataItem != null){
                    if (log.isDebugEnabled()) {
                        log.debug("The queue size is now " + queue.size() + ". Data just taken: ");
                        DevMonTask.printPacket(devDataItem.getData());
                    }
                    processDevData(devDataItem);
                } else {
                    log.warn("devDataItem is null. The queue size is now " + queue.size());
                }
            } catch (InterruptedException ex) {
                log.error("InterruptedException Caught, processDevData() is not called!", ex);
                if (stopped){ 
                    return;
                }
                continue;
            } catch (Exception e){
                log.info("Exception Caught, processDevData() is not called!", e);
                if(stopped){
                    return;
                }
                continue;
            }
        }
    }

    private void processDevData(DatagramPacket packet) {
        byte[] bytePacket = new byte[20];
        byte[] byteDevId = new byte[10];
        String strDevId = null;

        bytePacket = packet.getData();
        if (isValidPacket(bytePacket) == false) {
            return;
        }

        for (int i=0; i<10; i++) {
        	byteDevId[i] = bytePacket[4+i];
        }
        strDevId = new String(byteDevId, 0, byteDevId.length);
        
        if (bytePacket[1] == (byte)0xDB) {       	
    		byte[] byteResponseSU = new byte[512+9];
			int numFrame = DevMonTask.getNumFrame(byteDevId[1]);
			int intSize = DevMonTask.getImageSize(byteDevId[1]);
			int i = 0;
			
			DevMonTask.removeDevInSU(strDevId);
			
			for (i=0; i<numFrame && DevMonTask.notDevInSU(strDevId); i++) {	
				int j=0;
				// DB1 Header: always 0xE5
				byteResponseSU[0] = (byte) 0xE5;
				// DB2 Type: 0xF1(config data), 0xF2(reserved), 0xF3(ACK), 0xF4(image info), 0xF5(image data)
				byteResponseSU[1] = (byte)0xF5;
				// DB3 Frame number
				byteResponseSU[2] = (byte)((i + 1)&0xFF);
				// DB4 Data length and data
				if(i<numFrame-1) {
					byteResponseSU[3] = 2;
					byteResponseSU[4] = 0;
		            for (j=0; j<512; j++) {
						byteResponseSU[j+5] = DevMonTask.getImageData(byteDevId[1])[i*512+j];
					}
				} else {
					byteResponseSU[3] = (byte)((intSize%512)/256);
					byteResponseSU[4] = (byte)((intSize%512)%256);
					for (j=0; j<(intSize%512); j++) {
						byteResponseSU[j+5] = DevMonTask.getImageData(byteDevId[1])[i*512+j];
					}
				}

				// DB5 CRC
				if(i<numFrame-1) {
					byte[] byteCRCMsg = new byte[516];
					for (int k= 0; k < byteCRCMsg.length; k++){
						byteCRCMsg[k] = byteResponseSU[k+1];
					}
					short shortCRC = DevMonTask.getCrc16(byteCRCMsg, byteCRCMsg.length);
					byte[] byteCRC = DevMonTask.short2bytes(shortCRC);
					byteResponseSU[j+6] = byteCRC[1];
					byteResponseSU[j+7] = byteCRC[0];	
				} else {
					byte[] byteCRCMsg = new byte[intSize%512+4];
					for (int k= 0; k < byteCRCMsg.length; k++){
						byteCRCMsg[k] = byteResponseSU[k+1];
					}
					short shortCRC = DevMonTask.getCrc16(byteCRCMsg, byteCRCMsg.length);
					byte[] byteCRC = DevMonTask.short2bytes(shortCRC);
					byteResponseSU[j+6] = byteCRC[1];
					byteResponseSU[j+7] = byteCRC[0];	
				}
	
				// DB6 Tail
				byteResponseSU[j+8] = (byte)0xE6;
				
				InetAddress address = packet.getAddress();
				int port = packet.getPort();
				DatagramPacket responsePacket = new DatagramPacket(byteResponseSU, j+9, address, port);
	
				try{
					DevMonTask.serverSocket.send(responsePacket);
					Thread.sleep(200);
				} catch (IOException ioe){
					log.error("DevSUThread: Failed to send response to device.", ioe);
					return;
				} catch (Exception e){
					log.error("DevSUThread: Failed to send response to device.", e);
					return;
				}
			}
			return;
        } else {
			log.error("DevSUThread: Wong packet type is received: " + bytePacket[1]);
        }
    }

    private boolean isValidPacket(byte[] bytePacket){
        if (bytePacket[0] != (byte)0xE2) {
            log.warn("Received frame header is unknown, being ignored: " + bytePacket[0]);
            return false;
        }

        if (bytePacket[1] != (byte)0xAA && bytePacket[1] != (byte)0xBB && bytePacket[1] != (byte)0xCC && bytePacket[1] != (byte)0xDB) {
            log.warn("Received frame type is unknown, being ignored: " + bytePacket[1]);
            return false;
        }

        if (bytePacket[1] == (byte)0xAA && bytePacket[3] != (byte)0xF) {
            log.warn("Received frame lenth is incorrect, being ignored: type=0xAA, length=" + String.format("%02X ", bytePacket[3]) + "(should be 0xF)");
            return false;
        }

        if (bytePacket[1] == 0xBB && bytePacket[3] != (byte)0xE) {
            log.warn("Received frame lenth is incorrect, being ignored: type=0xBB, length=" + String.format("%02X ", bytePacket[3]) + "(should be 0xE)");
            return false;
        }

        if (bytePacket[1] == 0xCC && bytePacket[3] != (byte)0xA) {
            log.warn("Received frame lenth is incorrect, being ignored: type=0xCC, " + ", length=" + String.format("%02X ", bytePacket[3]) + "(should be 0xA)");
            return false;
        }

        if (bytePacket[1] == (byte)0xAA && bytePacket[21] != (byte)0xE3) {
            log.warn("Received frame tail is incorrect, being ignored: " + bytePacket[21]);
            return false;
        }

        if (bytePacket[1] == (byte)0xBB && bytePacket[20] != (byte)0xE3) {
            log.warn("Received frame tail is incorrect, being ignored: " + bytePacket[20]);
            return false;
        }

        if (bytePacket[1] == (byte)0xCC && bytePacket[16] != (byte)0xE3) {
            log.warn("Received frame tail is incorrect, being ignored: " + bytePacket[16]);
            return false;
        }

        return true;
    }

    public int getQueueSize() {
        return queue.size();
    }

    public String getName() {
        return thread.getName();
    }
}